<?php

// +----------------------------------------------------------------------

// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]

// +----------------------------------------------------------------------

// | Copyright (c) 2006-2013 http://thinkphp.cn All rights reserved.

// +----------------------------------------------------------------------

// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )

// +----------------------------------------------------------------------

// | Author: 麦当苗儿 <zuojiazi@vip.qq.com> <http://www.zjzit.cn>

// +----------------------------------------------------------------------

namespace Think;

class Upload{

	/**

	 * 默认上传配置

	 * @var array

	 */

    private $config = array(

        'mimes'         =>  array(), //允许上传的文件MiMe类型

        'maxSize'       =>  0, //上传的文件大小限制 (0-不做限制)

        'exts'          =>  array(), //允许上传的文件后缀

        'autoSub'       =>  true, //自动子目录保存文件

        'subName'       =>  array('date', 'Y-m-d'), //子目录创建方式，[0]-函数名，[1]-参数，多个参数使用数组

    	'rootPath'      =>     './Uploads/', //保存根路径

        'savePath'      =>  '', //保存路径

        'saveName'      =>  array('uniqid', ''), //上传文件命名规则，[0]-函数名，[1]-参数，多个参数使用数组

        'saveExt'       =>  '', //文件保存后缀，空则使用原后缀

        'replace'       =>  false, //存在同名是否覆盖

        'hash'          =>  true, //是否生成hash编码

        'callback'      =>  false, //检测文件是否存在回调，如果存在返回文件信息数组

        'driver'        =>  '', // 文件上传驱动

        'driverConfig'  =>  array(), // 上传驱动配置

    );



    /**

     * 上传错误信息

     * @var string

     */

    private $error = ''; //上传错误信息



    /**

     * 上传驱动实例

     * @var Object

     */

    private $uploader;



    /**

     * 构造方法，用于构造上传实例

     * @param array  $config 配置

     * @param string $driver 要使用的上传驱动 LOCAL-本地上传驱动，FTP-FTP上传驱动

     */

    public function __construct($config = array(), $driver = '', $driverConfig = null){

    	/* 获取配置 */

        $this->config   =   array_merge($this->config, $config);

        $driver         =   $driver? $driver : ($this->driver? $this->driver : C('FILE_UPLOAD_TYPE'));

        $driverConfig   =   $driverConfig? $driverConfig : ($this->driverConfig? $this->driverConfig :C('UPLOAD_TYPE_CONFIG'));



        /* 设置上传驱动 */

        $class      =   strpos($driver,'\\')? $driver : 'Think\\Upload\\Driver\\'.ucfirst(strtolower($driver));

    	$this->setDriver($class, $driverConfig);



        /* 调整配置，把字符串配置参数转换为数组 */

        if(!empty($this->config['mimes'])){

            if(is_string($this->mimes)) {

                $this->config['mimes'] = explode(',', $this->mimes);

            }

            $this->config['mimes'] = array_map('strtolower', $this->mimes);

        }

        if(!empty($this->config['exts'])){

            if (is_string($this->exts)){

                $this->config['exts'] = explode(',', $this->exts);

            }

            $this->config['exts'] = array_map('strtolower', $this->exts);

        }

    }



    /**

     * 使用 $this->name 获取配置

     * @param  string $name 配置名称

     * @return multitype    配置值

     */

    public function __get($name) {

        return $this->config[$name];

    }



    public function __set($name,$value){

        if(isset($this->config[$name])) {

            $this->config[$name]    =   $value;

        }

    }



    public function __isset($name){

        return isset($this->config[$name]);

    }



    /**

     * 获取最后一次上传错误信息

     * @return string 错误信息

     */

    public function getError(){

        return $this->error;

    }



    /**

     * 上传单个文件

     * @param  array  $file 文件数组

     * @return array        上传成功后的文件信息

     */

    public function uploadOne($file){

        $info = $this->upload(array($file));

        return $info ? $info[0] : $info;

    }



    /**

     * 上传文件

     * @param 文件信息数组 $files ，通常是 $_FILES数组

     */

    public function upload($files='') {

        if('' === $files){

            $files  =   $_FILES;

        }

        if(empty($files)){

            $this->error = '没有上传的文件！';

            return false;

        }



        /* 检测上传根目录 */

        if(!$this->uploader->checkRootPath()){

            $this->error = $this->uploader->getError();

            return false;

        }



        /* 检查上传目录 */

        if(!$this->uploader->checkSavePath($this->savePath)){

            $this->error = $this->uploader->getError();

            return false;

        }



        /* 逐个检测并上传文件 */

        $info    =  array();

        if(function_exists('finfo_open')){

            $finfo   =  finfo_open ( FILEINFO_MIME_TYPE );

        }

        // 对上传文件数组信息处理

        $files   =  $this->dealFiles($files);    

        foreach ($files as $key => $file) {

            if(!isset($file['key']))   $file['key']    =   $key;

            /* 通过扩展获取文件类型，可解决FLASH上传$FILES数组返回文件类型错误的问题 */

            if(isset($finfo)){

                $file['type']   =   finfo_file ( $finfo ,  $file['tmp_name'] );

            }



            /* 获取上传文件后缀，允许上传无后缀文件 */

            $file['ext']    =   pathinfo($file['name'], PATHINFO_EXTENSION);



            /* 文件上传检测 */

            if (!$this->check($file)){

                continue;

            }



            /* 获取文件hash */

            if($this->hash){

                $file['md5']  = md5_file($file['tmp_name']);

                $file['sha1'] = sha1_file($file['tmp_name']);

            }



            /* 调用回调函数检测文件是否存在 */

            $data = call_user_func($this->callback, $file);

            if( $this->callback && $data ){

                if ( file_exists('.'.$data['path'])  ) {

                    $info[$key] = $data;

                    continue;

                }elseif($this->removeTrash){

                    call_user_func($this->removeTrash,$data);//删除垃圾据

                }

            }



            /* 生成保存文件名 */

            $savename = $this->getSaveName($file);

            if(false == $savename){

                continue;

            } else {

                $file['savename'] = $savename;

            }



            /* 检测并创建子目录 */

            $subpath = $this->getSubPath($file['name']);

            if(false === $subpath){

                continue;

            } else {

                $file['savepath'] = $this->savePath . $subpath;

            }



            /* 对图像文件进行严格检测 */

            $ext = strtolower($file['ext']);

            if(in_array($ext, array('gif','jpg','jpeg','bmp','png','swf'))) {

                $imginfo = getimagesize($file['tmp_name']);

                if(empty($imginfo) || ($ext == 'gif' && empty($imginfo['bits']))){

                    $this->error = '非法图像文件！';

                    continue;

                }

            }



            /* 保存文件 并记录保存成功的文件 */

            if ($this->uploader->save($file,$this->replace)) {

                unset($file['error'], $file['tmp_name']);

                $info[$key] = $file;

            } else {

                $this->error = $this->uploader->getError();

            }

        }

        if(isset($finfo)){

            finfo_close($finfo);

        }

        return empty($info) ? false : $info;

    }



    /**

     * 转换上传文件数组变量为正确的方式

     * @access private

     * @param array $files  上传的文件变量

     * @return array

     */

    private function dealFiles($files) {

        $fileArray  = array();

        $n          = 0;

        foreach ($files as $key=>$file){

            if(is_array($file['name'])) {

                $keys       =   array_keys($file);

                $count      =   count($file['name']);

                for ($i=0; $i<$count; $i++) {

                    $fileArray[$n]['key'] = $key;

                    foreach ($keys as $_key){

                        $fileArray[$n][$_key] = $file[$_key][$i];

                    }

                    $n++;

                }

            }else{

               $fileArray[$key] = $file;

            }

        }

       return $fileArray;

    }



    /**

     * 设置上传驱动

     * @param string $class 驱动类名称

     */

    private function setDriver($class, $config){

        $this->uploader = new $class($this->rootPath, $config);

        if(!$this->uploader){

            E("不存在上传驱动：{$name}");

        }

    }



    /**

     * 检查上传的文件

     * @param array $file 文件信息

     */

    private function check($file) {

        /* 文件上传失败，捕获错误代码 */

        if ($file['error']) {

            $this->error($file['error']);

            return false;

        }



        /* 无效上传 */

        if (empty($file['name'])){

            $this->error = '未知上传错误！';

        }



        /* 检查是否合法上传 */

        if (!is_uploaded_file($file['tmp_name'])) {

            $this->error = '非法上传文件！';

            return false;

        }



        /* 检查文件大小 */

        if (!$this->checkSize($file['size'])) {

            $this->error = '上传文件大小不符！';

            return false;

        }



        /* 检查文件Mime类型 */

        //TODO:FLASH上传的文件获取到的mime类型都为application/octet-stream

        if (!$this->checkMime($file['type'])) {

            $this->error = '上传文件MIME类型不允许！';

            return false;

        }



        /* 检查文件后缀 */

        if (!$this->checkExt($file['ext'])) {

            $this->error = '上传文件后缀不允许';

            return false;

        }



        /* 通过检测 */

        return true;

    }





    /**

     * 获取错误代码信息

     * @param string $errorNo  错误号

     */

    private function error($errorNo) {

        switch ($errorNo) {

            case 1:

                $this->error = '上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值！';

                break;

            case 2:

                $this->error = '上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值！';

                break;

            case 3:

                $this->error = '文件只有部分被上传！';

                break;

            case 4:

                $this->error = '没有文件被上传！';

                break;

            case 6:

                $this->error = '找不到临时文件夹！';

                break;

            case 7:

                $this->error = '文件写入失败！';

                break;

            default:

                $this->error = '未知上传错误！';

        }

    }



    /**

     * 检查文件大小是否合法

     * @param integer $size 数据

     */

    private function checkSize($size) {

        return !($size > $this->maxSize) || (0 == $this->maxSize);

    }



    /**

     * 检查上传的文件MIME类型是否合法

     * @param string $mime 数据

     */

    private function checkMime($mime) {

        return empty($this->config['mimes']) ? true : in_array(strtolower($mime), $this->mimes);

    }



    /**

     * 检查上传的文件后缀是否合法

     * @param string $ext 后缀

     */

    private function checkExt($ext) {

        return empty($this->config['exts']) ? true : in_array(strtolower($ext), $this->exts);

    }



    /**

     * 根据上传文件命名规则取得保存文件名

     * @param string $file 文件信息

     */

    private function getSaveName($file) {

        $rule = $this->saveName;

        if (empty($rule)) { //保持文件名不变

            /* 解决pathinfo中文文件名BUG */

            $filename = substr(pathinfo("_{$file['name']}", PATHINFO_FILENAME), 1);

            $savename = $filename;

        } else {

            $savename = $this->getName($rule, $file['name']);

            if(empty($savename)){

                $this->error = '文件命名规则错误！';

                return false;

            }

        }



        /* 文件保存后缀，支持强制更改文件后缀 */

        $ext = empty($this->config['saveExt']) ? $file['ext'] : $this->saveExt;



        return $savename . '.' . $ext;

    }



    /**

     * 获取子目录的名称

     * @param array $file  上传的文件信息

     */

    private function getSubPath($filename) {

        $subpath = '';

        $rule    = $this->subName;

        if ($this->autoSub && !empty($rule)) {

            $subpath = $this->getName($rule, $filename) . '/';



            if(!empty($subpath) && !$this->uploader->mkdir($this->savePath . $subpath)){

                $this->error = $this->uploader->getError();

                return false;

            }

        }

        return $subpath;

    }



    /**

     * 根据指定的规则获取文件或目录名称

     * @param  array  $rule     规则

     * @param  string $filename 原文件名

     * @return string           文件或目录名称

     */

    private function getName($rule, $filename){

        $name = '';

        if(is_array($rule)){ //数组规则

            $func     = $rule[0];

            $param    = (array)$rule[1];

            foreach ($param as &$value) {

               $value = str_replace('__FILE__', $filename, $value);

            }

            $name = call_user_func_array($func, $param);

        } elseif (is_string($rule)){ //字符串规则

            if(function_exists($rule)){

                $name = call_user_func($rule);

            } else {

                $name = $rule;

            }

        }

        return $name;

    }



}

